package nl.rug.jvleusen.datastructurelib.set;

/**
 * The NDimensionalArray is a adapter on top of an array to allow for specified
 * dimensions within an array, after initialization the size is unchangeable. Do
 * keep in mind that this is for specific data storage, it is not possible to
 * check for elements nor enumerate them unless this class is extended with that
 * feature.
 *
 * @author Jeroen van Leusen
 */
public class NDimensionalArray<E> {

    private final E[] array;
    private final int[] dimensions;
    private final int[] strides;
    private final int size;

    /**
     * Creates an instance of this class with the specified dimensions,
     * dimensions are resolved from left to right, same as with a normal array.
     *
     * @param dimensions Integer signifying the size of each dimension
     */
    public NDimensionalArray(int... dimensions) {
        if (dimensions.length <= 0) {
            throw new IllegalArgumentException("Invalid indices");
        }

        for (int i = 0; i < dimensions.length; i++) {
            if (dimensions[i] <= 0) {
                throw new IllegalArgumentException("Negative indice.");
            }
        }

        this.strides = new int[dimensions.length];
        this.dimensions = dimensions;

        this.strides[0] = 1;
        int s = dimensions[0];
        for (int i = 1; i < dimensions.length; i++) {
            this.strides[i] = this.strides[i - 1] * dimensions[i - 1];
            s *= dimensions[i];
        }
        this.size = s;
        this.array = (E[]) new Object[size()];
    }

    /**
     * Attempts to get the value at the location specified by the indices.
     *
     * @param indices Indices of each dimension specifying the location to look
     * for the data.
     * @return Whatever value was set to the specified location, this is {@code null}
     * by default.
     * @throws IllegalArgumentException If the number of indices is not equal to
     * the amount of dimensions.
     * @throws ArrayIndexOutOfBoundsException If an indice goes outside the
     * bounds of its dimension.
     */
    public E get(int... indices) {
        rangeCheck(indices);

        return get(getInternalIndex(indices));
    }

    private E get(int index) {
        return array[index];
    }

    /**
     * Attempts to put the specified data in the location specified by the
     * indices, the amount of indices is required to match {@code NDimensionalArray#N()}
     * and be within the bounds of each dimension.
     *
     * @param data Data to put into the array
     * @param indices Indices of each dimension specifying the location to put
     * the data.
     * @throws IllegalArgumentException If the number of indices is not equal to
     * the amount of dimensions.
     * @throws ArrayIndexOutOfBoundsException If an indice goes outside the
     * bounds of its dimension.
     */
    public void put(E data, int... indices) {
        rangeCheck(indices);

        set(getInternalIndex(indices), data);
    }

    private void set(int index, E value) {
        array[index] = value;
    }

    /**
     * Returns the total number of dimensions this instance has, this is also
     * the required number of indices to retrieve and use this instance.
     *
     * @return The number of dimensions
     */
    public int N() {
        return dimensions.length;
    }

    /**
     * Returns the size of a specified dimension, 1-indexed.
     *
     * @param i Which dimension to retrieve the size of.
     * @return The size of the i-th dimension.
     * @throws ArrayIndexOutOfBoundsException If i > {@link NDimensionalArray#N()}
     * or i <= 0
     */
    public int dimensionSize(int i) {
        return dimensions[i - 1];
    }

    private void rangeCheck(int[] indices) {
        if (indices.length != dimensions.length) {
            throw new IllegalArgumentException("Insufficient arguments to point to an exact point.");
        }

        for (int i = 0; i < dimensions.length; i++) {
            if (indices[i] >= dimensions[i]) { //0-indexed, so indice has to be smaller than the actual dimension
                throw new ArrayIndexOutOfBoundsException(String.format("%dth dimension, index %s > size %s", i + 1, indices[i], dimensions[i]));
            }
            if (indices[i] < 0) {
                throw new ArrayIndexOutOfBoundsException(String.format("Negative indice for dimension %d", i + 1));
            }
        }
    }

    private int getInternalIndex(int[] indices) {
        int internalIndex = indices[0];
        for (int i = 1; i < indices.length; ++i) {
            internalIndex += indices[i] * strides[i];
        }
        return internalIndex;
    }

    /**
     * Will return the total number of elements in this set can contain, this
     * number will never change.
     *
     * @return The total size of all dimensions.
     */
    public int size() {
        return size;
    }

    /**
     * Fills the array with a specific value, this will overwrite any old
     * values.
     *
     * @param value Element to fill the array with
     */
    public void fill(E value) {
        for (int i = 0; i < size; ++i) {
            set(i, value);
        }
    }
}
